@@ -8,7 +8,7 @@ module Agents |
||
| 8 | 8 |
|
| 9 | 9 |
This Agent will output data at: |
| 10 | 10 |
|
| 11 |
- `https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret.xml`
|
|
| 11 |
+ `https://#{ENV['DOMAIN']}#{Rails.application.routes.url_helpers.web_requests_path(agent_id: ':id', user_id: user_id, secret: ':secret', format: :xml)}`
|
|
| 12 | 12 |
|
| 13 | 13 |
where `:secret` is one of the allowed secrets specified in your options and the extension can be `xml` or `json`. |
| 14 | 14 |
|
@@ -19,9 +19,9 @@ module Agents |
||
| 19 | 19 |
|
| 20 | 20 |
* `secrets` - An array of tokens that the requestor must provide for light-weight authentication. |
| 21 | 21 |
* `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents. |
| 22 |
- * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given. |
|
| 22 |
+ * `template` - A JSON object representing a mapping between item output keys and incoming event values. Use [Liquid](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to format the values. Values of the `link`, `title`, `description` and `icon` keys will be put into the \\<channel\\> section of RSS output. The `item` key will be repeated for every Event. The `pubDate` key for each item will have the creation time of the Event unless given. |
|
| 23 | 23 |
* `events_to_show` - The number of events to output in RSS or JSON. (default: `40`) |
| 24 |
- * `ttl` - A value for the <ttl> element in RSS output. (default: `60`) |
|
| 24 |
+ * `ttl` - A value for the \\<ttl\\> element in RSS output. (default: `60`) |
|
| 25 | 25 |
|
| 26 | 26 |
If you'd like to output RSS tags with attributes, such as `enclosure`, use something like the following in your `template`: |
| 27 | 27 |
|
@@ -39,6 +39,13 @@ module Agents |
||
| 39 | 39 |
}, |
| 40 | 40 |
"_contents": "tag contents (can be an object for nesting)" |
| 41 | 41 |
} |
| 42 |
+ |
|
| 43 |
+ # Liquid Templating |
|
| 44 |
+ |
|
| 45 |
+ In Liquid templating, the following variable is available: |
|
| 46 |
+ |
|
| 47 |
+ * `events`: An array of events being output, sorted in descending order up to `events_to_show` in number. For example, if source events contain a site title in the `site_title` key, you can refer to it in `template.title` by putting `{{events.first.site_title}}`.
|
|
| 48 |
+ |
|
| 42 | 49 |
MD |
| 43 | 50 |
end |
| 44 | 51 |
|
@@ -63,7 +70,17 @@ module Agents |
||
| 63 | 70 |
end |
| 64 | 71 |
|
| 65 | 72 |
def validate_options |
| 66 |
- unless options['secrets'].is_a?(Array) && options['secrets'].length > 0 |
|
| 73 |
+ if options['secrets'].is_a?(Array) && options['secrets'].length > 0 |
|
| 74 |
+ options['secrets'].each do |secret| |
|
| 75 |
+ case secret |
|
| 76 |
+ when %r{[/.]}
|
|
| 77 |
+ errors.add(:base, "secret may not contain a slash or dot") |
|
| 78 |
+ when String |
|
| 79 |
+ else |
|
| 80 |
+ errors.add(:base, "secret must be a string") |
|
| 81 |
+ end |
|
| 82 |
+ end |
|
| 83 |
+ else |
|
| 67 | 84 |
errors.add(:base, "Please specify one or more secrets for 'authenticating' incoming feed requests") |
| 68 | 85 |
end |
| 69 | 86 |
|
@@ -92,15 +109,39 @@ module Agents |
||
| 92 | 109 |
interpolated['template']['link'].presence || "https://#{ENV['DOMAIN']}"
|
| 93 | 110 |
end |
| 94 | 111 |
|
| 112 |
+ def feed_url(options = {})
|
|
| 113 |
+ feed_link + Rails.application.routes.url_helpers. |
|
| 114 |
+ web_requests_path(agent_id: id || ':id', |
|
| 115 |
+ user_id: user_id, |
|
| 116 |
+ secret: options[:secret], |
|
| 117 |
+ format: options[:format]) |
|
| 118 |
+ end |
|
| 119 |
+ |
|
| 120 |
+ def feed_icon |
|
| 121 |
+ interpolated['template']['icon'].presence || feed_link + '/favicon.ico' |
|
| 122 |
+ end |
|
| 123 |
+ |
|
| 95 | 124 |
def feed_description |
| 96 | 125 |
interpolated['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent"
|
| 97 | 126 |
end |
| 98 | 127 |
|
| 99 | 128 |
def receive_web_request(params, method, format) |
| 100 |
- if interpolated['secrets'].include?(params['secret']) |
|
| 101 |
- items = received_events.order('id desc').limit(events_to_show).map do |event|
|
|
| 129 |
+ unless interpolated['secrets'].include?(params['secret']) |
|
| 130 |
+ if format =~ /json/ |
|
| 131 |
+ return [{ error: "Not Authorized" }, 401]
|
|
| 132 |
+ else |
|
| 133 |
+ return ["Not Authorized", 401] |
|
| 134 |
+ end |
|
| 135 |
+ end |
|
| 136 |
+ |
|
| 137 |
+ source_events = received_events.order(id: :desc).limit(events_to_show).to_a |
|
| 138 |
+ |
|
| 139 |
+ interpolation_context.stack do |
|
| 140 |
+ interpolation_context['events'] = source_events |
|
| 141 |
+ |
|
| 142 |
+ items = source_events.map do |event| |
|
| 102 | 143 |
interpolated = interpolate_options(options['template']['item'], event) |
| 103 |
- interpolated['guid'] = {'_attributes' => {'isPermaLink' => 'false'},
|
|
| 144 |
+ interpolated['guid'] = {'_attributes' => {'isPermaLink' => 'false'},
|
|
| 104 | 145 |
'_contents' => interpolated['guid'].presence || event.id} |
| 105 | 146 |
date_string = interpolated['pubDate'].to_s |
| 106 | 147 |
date = |
@@ -128,12 +169,13 @@ module Agents |
||
| 128 | 169 |
<?xml version="1.0" encoding="UTF-8" ?> |
| 129 | 170 |
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
| 130 | 171 |
<channel> |
| 131 |
- <atom:link href="#{feed_link.encode(:xml => :text)}/users/#{user.id}/web_requests/#{id || '<id>'}/#{params['secret']}.xml" rel="self" type="application/rss+xml" />
|
|
| 132 |
- <title>#{feed_title.encode(:xml => :text)}</title>
|
|
| 133 |
- <description>#{feed_description.encode(:xml => :text)}</description>
|
|
| 134 |
- <link>#{feed_link.encode(:xml => :text)}</link>
|
|
| 135 |
- <lastBuildDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</lastBuildDate>
|
|
| 136 |
- <pubDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</pubDate>
|
|
| 172 |
+ <atom:link href=#{feed_url(secret: params['secret'], format: :xml).encode(xml: :attr)} rel="self" type="application/rss+xml" />
|
|
| 173 |
+ <atom:icon>#{feed_icon.encode(xml: :text)}</atom:icon>
|
|
| 174 |
+ <title>#{feed_title.encode(xml: :text)}</title>
|
|
| 175 |
+ <description>#{feed_description.encode(xml: :text)}</description>
|
|
| 176 |
+ <link>#{feed_link.encode(xml: :text)}</link>
|
|
| 177 |
+ <lastBuildDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</lastBuildDate>
|
|
| 178 |
+ <pubDate>#{Time.now.rfc2822.to_s.encode(xml: :text)}</pubDate>
|
|
| 137 | 179 |
<ttl>#{feed_ttl}</ttl>
|
| 138 | 180 |
|
| 139 | 181 |
XML |
@@ -147,12 +189,6 @@ module Agents |
||
| 147 | 189 |
|
| 148 | 190 |
return [content, 200, 'text/xml'] |
| 149 | 191 |
end |
| 150 |
- else |
|
| 151 |
- if format =~ /json/ |
|
| 152 |
- return [{ :error => "Not Authorized" }, 401]
|
|
| 153 |
- else |
|
| 154 |
- return ["Not Authorized", 401] |
|
| 155 |
- end |
|
| 156 | 192 |
end |
| 157 | 193 |
end |
| 158 | 194 |
|
@@ -34,8 +34,18 @@ describe Agents::DataOutputAgent do |
||
| 34 | 34 |
expect(agent).not_to be_valid |
| 35 | 35 |
agent.options[:secrets] = "foo" |
| 36 | 36 |
expect(agent).not_to be_valid |
| 37 |
+ agent.options[:secrets] = "foo/bar" |
|
| 38 |
+ expect(agent).not_to be_valid |
|
| 39 |
+ agent.options[:secrets] = "foo.xml" |
|
| 40 |
+ expect(agent).not_to be_valid |
|
| 41 |
+ agent.options[:secrets] = false |
|
| 42 |
+ expect(agent).not_to be_valid |
|
| 37 | 43 |
agent.options[:secrets] = [] |
| 38 | 44 |
expect(agent).not_to be_valid |
| 45 |
+ agent.options[:secrets] = ["foo.xml"] |
|
| 46 |
+ expect(agent).not_to be_valid |
|
| 47 |
+ agent.options[:secrets] = ["hello", true] |
|
| 48 |
+ expect(agent).not_to be_valid |
|
| 39 | 49 |
agent.options[:secrets] = ["hello"] |
| 40 | 50 |
expect(agent).to be_valid |
| 41 | 51 |
agent.options[:secrets] = ["hello", "world"] |
@@ -83,9 +93,10 @@ describe Agents::DataOutputAgent do |
||
| 83 | 93 |
expect(status).to eq(200) |
| 84 | 94 |
end |
| 85 | 95 |
|
| 86 |
- describe "outputtng events as RSS and JSON" do |
|
| 96 |
+ describe "outputting events as RSS and JSON" do |
|
| 87 | 97 |
let!(:event1) do |
| 88 | 98 |
agents(:bob_website_agent).create_event :payload => {
|
| 99 |
+ "site_title" => "XKCD", |
|
| 89 | 100 |
"url" => "http://imgs.xkcd.com/comics/evolving.png", |
| 90 | 101 |
"title" => "Evolving", |
| 91 | 102 |
"hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution." |
@@ -94,6 +105,7 @@ describe Agents::DataOutputAgent do |
||
| 94 | 105 |
|
| 95 | 106 |
let!(:event2) do |
| 96 | 107 |
agents(:bob_website_agent).create_event :payload => {
|
| 108 |
+ "site_title" => "XKCD", |
|
| 97 | 109 |
"url" => "http://imgs.xkcd.com/comics/evolving2.png", |
| 98 | 110 |
"title" => "Evolving again", |
| 99 | 111 |
"date" => '', |
@@ -103,6 +115,7 @@ describe Agents::DataOutputAgent do |
||
| 103 | 115 |
|
| 104 | 116 |
let!(:event3) do |
| 105 | 117 |
agents(:bob_website_agent).create_event :payload => {
|
| 118 |
+ "site_title" => "XKCD", |
|
| 106 | 119 |
"url" => "http://imgs.xkcd.com/comics/evolving0.png", |
| 107 | 120 |
"title" => "Evolving yet again with a past date", |
| 108 | 121 |
"date" => '2014/05/05', |
@@ -119,7 +132,8 @@ describe Agents::DataOutputAgent do |
||
| 119 | 132 |
<?xml version="1.0" encoding="UTF-8" ?> |
| 120 | 133 |
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
| 121 | 134 |
<channel> |
| 122 |
- <atom:linkhref="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
|
|
| 135 |
+ <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
|
|
| 136 |
+ <atom:icon>https://yoursite.com/favicon.ico</atom:icon> |
|
| 123 | 137 |
<title>XKCD comics as a feed</title> |
| 124 | 138 |
<description>This is a feed of recent XKCD comics, generated by Huginn</description> |
| 125 | 139 |
<link>https://yoursite.com</link> |
@@ -194,6 +208,43 @@ describe Agents::DataOutputAgent do |
||
| 194 | 208 |
] |
| 195 | 209 |
}) |
| 196 | 210 |
end |
| 211 |
+ |
|
| 212 |
+ describe "interpolating \"events\"" do |
|
| 213 |
+ before do |
|
| 214 |
+ agent.options['template']['title'] = "XKCD comics as a feed{% if events.first.site_title %} ({{events.first.site_title}}){% endif %}"
|
|
| 215 |
+ agent.save! |
|
| 216 |
+ end |
|
| 217 |
+ |
|
| 218 |
+ it "can output RSS" do |
|
| 219 |
+ stub(agent).feed_link { "https://yoursite.com" }
|
|
| 220 |
+ content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
|
|
| 221 |
+ expect(status).to eq(200) |
|
| 222 |
+ expect(content_type).to eq('text/xml')
|
|
| 223 |
+ expect(Nokogiri(content).at('/rss/channel/title/text()').text).to eq('XKCD comics as a feed (XKCD)')
|
|
| 224 |
+ end |
|
| 225 |
+ |
|
| 226 |
+ it "can output JSON" do |
|
| 227 |
+ content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
|
|
| 228 |
+ expect(status).to eq(200) |
|
| 229 |
+ |
|
| 230 |
+ expect(content['title']).to eq('XKCD comics as a feed (XKCD)')
|
|
| 231 |
+ end |
|
| 232 |
+ end |
|
| 233 |
+ |
|
| 234 |
+ describe "with a specified icon" do |
|
| 235 |
+ before do |
|
| 236 |
+ agent.options['template']['icon'] = 'https://somesite.com/icon.png' |
|
| 237 |
+ agent.save! |
|
| 238 |
+ end |
|
| 239 |
+ |
|
| 240 |
+ it "can output RSS" do |
|
| 241 |
+ stub(agent).feed_link { "https://yoursite.com" }
|
|
| 242 |
+ content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
|
|
| 243 |
+ expect(status).to eq(200) |
|
| 244 |
+ expect(content_type).to eq('text/xml')
|
|
| 245 |
+ expect(Nokogiri(content).at('/rss/channel/atom:icon/text()').text).to eq('https://somesite.com/icon.png')
|
|
| 246 |
+ end |
|
| 247 |
+ end |
|
| 197 | 248 |
end |
| 198 | 249 |
|
| 199 | 250 |
describe "outputting nesting" do |
@@ -294,7 +345,8 @@ describe Agents::DataOutputAgent do |
||
| 294 | 345 |
<?xml version="1.0" encoding="UTF-8" ?> |
| 295 | 346 |
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
| 296 | 347 |
<channel> |
| 297 |
- <atom:linkhref="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
|
|
| 348 |
+ <atom:link href="https://yoursite.com/users/#{agent.user.id}/web_requests/#{agent.id}/secret1.xml" rel="self" type="application/rss+xml"/>
|
|
| 349 |
+ <atom:icon>https://yoursite.com/favicon.ico</atom:icon> |
|
| 298 | 350 |
<title>XKCD comics as a feed</title> |
| 299 | 351 |
<description>This is a feed of recent XKCD comics, generated by Huginn</description> |
| 300 | 352 |
<link>https://yoursite.com</link> |